/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.server.core.event;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;

import org.bmi.gwt.mi.server.annotation.ClassMiHandler;
import org.bmi.gwt.mi.server.annotation.ObjectMiHandler;
import org.bmi.gwt.mi.server.channel.ChannelFactory;
import org.bmi.gwt.mi.server.core.queue.MiQueue;
import org.bmi.gwt.mi.server.core.queue.QueueExpressionUtil;
import org.bmi.gwt.mi.server.events.MiEvent;
import org.bmi.gwt.mi.server.events.MiEventHandler;
import org.bmi.gwt.mi.server.events.QueueEventHandler;
import org.bmi.gwt.mi.server.util.ServerUtil;
import org.bmi.gwt.mi.shared.communication.call.Call;
import org.bmi.gwt.mi.shared.communication.call.ConstructorCall;
import org.bmi.gwt.mi.shared.communication.call.FactoryCall;
import org.bmi.gwt.mi.shared.communication.call.MethodCall;
import org.bmi.gwt.mi.shared.exceptions.NameParseException;
import org.bmi.gwt.mi.shared.util.Constants;

/**
 * Main interface to enable client code to provide handlers like
 * {@link MiEventHandler} and {@link QueueEventHandler} and notify when handled
 * elements ({@link MiQueue} or method execution related events) require to fire
 * events.
 * 
 * @author marco.tamburelli@gmail.com
 */
public class HandlersManager
{
	private ExecutorService executor;
	private ChannelFactory channelFactory;

	private Map<String, List<MiEventHandler>> objMiHandlers;
	private Map<String, List<MiEventHandler>> classMiHandlers;
	private Map<String, List<QueueEventHandler>> queueEventHandlers;

	private ServletContext servletContext;

	public HandlersManager(ServletContext servletContext)
	{
		this.objMiHandlers = new HashMap<String, List<MiEventHandler>>();
		this.classMiHandlers = new HashMap<String, List<MiEventHandler>>();
		this.queueEventHandlers = new HashMap<String, List<QueueEventHandler>>();

		this.executor = Executors.newCachedThreadPool();
		this.servletContext = servletContext;
	}

	public void setChannelfactory(ChannelFactory channelfactory)
	{
		this.channelFactory = channelfactory;
	}

	/**
	 * Executes asynchronously the provided command.
	 * 
	 * @param command A command.
	 */
	public void asyncExecute(Runnable command)
	{
		this.executor.execute(command);
	}

	/**
	 * Notifies all active handlers that the provided queue has been created.
	 * 
	 * @param q A queue.
	 * @param async When <code>true</code> the execution of handlers is
	 *        asynchronous.
	 */
	public void fireQueueCreate(MiQueue q, boolean async)
	{
		if (this.channelFactory == null) return;

		String queueName = q.getName();
		String queueExpression = queueName;

		while (queueExpression != null)
		{
			if (this.queueEventHandlers.containsKey(queueExpression))
				for (final QueueEventHandler handler : this.queueEventHandlers.get(queueExpression))
				{
					final QueueEventImpl createEvent = new QueueEventImpl(this.channelFactory, queueName,
							this.servletContext);

					if (async)
						this.executor.execute(new Runnable()
						{
							@Override
							public void run()
							{
								handler.onCreate(createEvent);
							}
						});
					else
						handler.onCreate(createEvent);
				}

			queueExpression = QueueExpressionUtil.getMatcher(queueExpression);
		}
	}

	/**
	 * Notifies all active handlers that the provided queue has been bound.
	 * 
	 * @param q A queue.
	 * @param async When <code>true</code> the execution of handlers is
	 *        asynchronous.
	 */
	public void fireQueueBound(MiQueue q, boolean async)
	{
		if (this.channelFactory == null) return;

		String queueName = q.getName();
		String queueExpression = queueName;

		while (queueExpression != null)
		{
			if (this.queueEventHandlers.containsKey(queueExpression))
				for (final QueueEventHandler handler : this.queueEventHandlers.get(queueExpression))
				{
					final QueueEventImpl createEvent = new QueueEventImpl(this.channelFactory, queueName,
							this.servletContext);

					if (async)
						this.executor.execute(new Runnable()
						{
							@Override
							public void run()
							{
								handler.onBound(createEvent);
							}
						});
					else
						handler.onBound(createEvent);
				}

			queueExpression = QueueExpressionUtil.getMatcher(queueExpression);
		}
	}

	/**
	 * Notifies all active handlers that some attribute in the provided queue
	 * has been updated.
	 * 
	 * @param q A queue.
	 * @param async When <code>true</code> the execution of handlers is
	 *        asynchronous.
	 */
	public void fireQueueUpdate(MiQueue q, boolean async)
	{
		if (this.channelFactory == null) return;

		String queueName = q.getName();
		String queueExpression = queueName;

		while (queueExpression != null)
		{
			if (this.queueEventHandlers.containsKey(queueExpression))
				for (final QueueEventHandler handler : this.queueEventHandlers.get(queueExpression))
				{
					final QueueEventImpl updateEvent = new QueueEventImpl(this.channelFactory, queueName,
							this.servletContext);

					if (async)
						this.executor.execute(new Runnable()
						{
							@Override
							public void run()
							{
								handler.onUpdate(updateEvent);
							}
						});
					else
						handler.onUpdate(updateEvent);
				}

			queueExpression = QueueExpressionUtil.getMatcher(queueExpression);
		}
	}

	/**
	 * Notifies all active handlers that the provided queue has been disposed.
	 * 
	 * @param q A queue.
	 * @param async When <code>true</code> the execution of handlers is
	 *        asynchronous.
	 */
	public void fireQueueDispose(MiQueue q, boolean async)
	{
		if (this.channelFactory == null) return;

		String queueName = q.getName();
		String queueExpression = queueName;

		while (queueExpression != null)
		{
			if (this.queueEventHandlers.containsKey(queueExpression))
				for (final QueueEventHandler handler : this.queueEventHandlers.get(queueExpression))
				{
					final QueueEventImpl disposeEvent = new QueueEventImpl(this.channelFactory, queueName,
							this.servletContext);

					if (async)
						this.executor.execute(new Runnable()
						{
							@Override
							public void run()
							{
								handler.onDispose(disposeEvent);
							}
						});
					else
						handler.onDispose(disposeEvent);
				}

			queueExpression = QueueExpressionUtil.getMatcher(queueExpression);
		}
	}

	/**
	 * For internal use.
	 */
	public final class MiEventExecutor
	{
		private final Call call;
		private final String queueName;

		private boolean canConsume;
		private List<MiEventHandlerEnvelope> matchedHandlers;

		private class MiEventHandlerEnvelope
		{
			MiEventHandler handler;
			Class<?>[] argTypes;
		}

		private MiEventExecutor(String queueName, Call call)
		{
			this.call = call;
			this.queueName = queueName;

			this.canConsume = true;
			this.matchedHandlers = new ArrayList<MiEventHandlerEnvelope>();
		}

		/**
		 * Handle method invocation event before the method could be consumed by
		 * a peer.
		 */
		public void executeOnBefores()
		{
			List<MiEventHandler> objMiHandlers = HandlersManager.this.objMiHandlers.get(this.call.getObjectId());

			/*
			 * If there are defined handlers for such object their
			 * onBeforeMethod method will be executed , before pushing the
			 * method ...
			 */
			if (objMiHandlers != null) for (MiEventHandler handler : objMiHandlers)
			{
				Class<? extends MiEventHandler> clazz = handler.getClass();
				ObjectMiHandler anno = clazz.getAnnotation(ObjectMiHandler.class);

				boolean allowed = false;

				for (String expr : anno.allowedQueues())
					if (QueueExpressionUtil.match(this.queueName, expr))
					{
						allowed = true;
						break;
					}

				if (!allowed) return;

				executeOnBefores(handler, anno.methodName(), anno.parameterTypes(), clazz);
			}

			String className = this.call.getTargetClassName();

			if (className == null) return;

			List<MiEventHandler> classMiHandlers = HandlersManager.this.classMiHandlers.get(className);

			if (classMiHandlers != null) for (MiEventHandler handler : classMiHandlers)
			{
				Class<? extends MiEventHandler> handlerClass = handler.getClass();
				ClassMiHandler anno = handlerClass.getAnnotation(ClassMiHandler.class);

				boolean allowed = false;

				for (String expr : anno.allowedQueues())
					if (QueueExpressionUtil.match(this.queueName, expr))
					{
						allowed = true;
						break;
					}

				if (!allowed) return;

				executeOnBefores(handler, anno.methodName(), anno.parameterTypes(), handlerClass);
			}
		}

		private void executeOnBefores(MiEventHandler handler, String methodName, Class<?>[] argTypes,
				Class<? extends MiEventHandler> clazz)
		{
			String expectedSignature = ServerUtil.getJNISignature(argTypes);
			String callMethod = null;
			String signature = null;

			if (this.call instanceof FactoryCall)
			{
				callMethod = ((FactoryCall) this.call).getFactoryMethod();
				signature = ((FactoryCall) this.call).getArgsSignature();
			}
			if (this.call instanceof MethodCall)
			{
				callMethod = ((MethodCall) this.call).getMethod();
				signature = ((MethodCall) this.call).getArgsSignature();
			}
			if (this.call instanceof ConstructorCall)
			{
				callMethod = Constants.CONSTRUCTOR;
				signature = ((ConstructorCall) this.call).getArgsSignature();
			}

			if (callMethod != null && callMethod.equals(methodName) && expectedSignature.equals(signature))
			{
				AbstractMiEvent beforeEvent = new AbstractMiEvent(this.queueName, this.call, argTypes,
						HandlersManager.this.channelFactory, HandlersManager.this.servletContext)
				{
					private boolean b = true;

					@Override
					public void setEnabled(boolean b)
					{
						this.b = b;
					}

					@Override
					public boolean isEnabled()
					{
						return this.b;
					}
				};

				handler.onBeforeMethod(beforeEvent);
				MiEventHandlerEnvelope x = new MiEventHandlerEnvelope();
				x.handler = handler;
				x.argTypes = argTypes;

				this.matchedHandlers.add(x);

				this.canConsume = this.canConsume && beforeEvent.isEnabled();
			}
		}

		/**
		 * Returns <code>true</code> when the method invocation can be consumed.
		 * It depends on the {@link MiEvent} state.
		 * 
		 * @return
		 */
		public boolean canConsume()
		{
			return this.canConsume;
		}

		/**
		 * Handle method invocation event after the method has been consumed by
		 * a peer.
		 * 
		 * Note that if the method consumption is not allowed (when
		 * {@link #canConsume()} returns <code>false</code> this method has no
		 * effect.
		 */
		public void executedOnAfters()
		{
			if (!this.canConsume) return;

			/*
			 * ...and in case handlers where found for such object their
			 * onAfterMethod will be executed.
			 */
			for (MiEventHandlerEnvelope x : this.matchedHandlers)
			{
				AbstractMiEvent afterEvent = new AbstractMiEvent(this.queueName, this.call, x.argTypes,
						HandlersManager.this.channelFactory, HandlersManager.this.servletContext)
				{
					@Override
					public void setEnabled(boolean enabled)
					{}

					@Override
					public boolean isEnabled()
					{
						return true;
					}
				};

				x.handler.onAfterMethod(afterEvent);
			}
		}
	}

	/**
	 * Method invocation events can't be properly fired since the execution of
	 * {@link MiEventHandler#onAfterMethod(org.bmi.gwt.mi.server.events.MiEvent)}
	 * can be decided only after the code of
	 * {@link MiEventHandler#onBeforeMethod(org.bmi.gwt.mi.server.events.MiEvent)}
	 * .
	 * 
	 * The proposed solution is to provide an external executor which can be
	 * aware of the results of the
	 * {@link MiEventHandler#onBeforeMethod(org.bmi.gwt.mi.server.events.MiEvent)}
	 * execution.
	 * 
	 * @param queueName The name of the target queue.
	 * @param call The handled call.
	 * @return
	 */
	public MiEventExecutor prepareMiEvents(String queueName, Call call)
	{
		return new MiEventExecutor(queueName, call);
	}

	/**
	 * This method add a method invocation handler to an object represented by
	 * its ID.
	 * 
	 * @param objectId &The object Id.
	 * @param handler The handler.
	 */
	public void addObjMiHandler(String objectId, MiEventHandler handler)
	{
		if (!this.objMiHandlers.containsKey(objectId))
			this.objMiHandlers.put(objectId, new ArrayList<MiEventHandler>());

		this.objMiHandlers.get(objectId).add(handler);
	}

	/**
	 * This method add a method invocation handler to any object defined by a
	 * provided class name.
	 * 
	 * @param className The class name.
	 * @param handler The handler.
	 */
	public void addClassMiHandler(String className, MiEventHandler handler)
	{
		if (!this.classMiHandlers.containsKey(className))
			this.classMiHandlers.put(className, new ArrayList<MiEventHandler>());

		this.classMiHandlers.get(className).add(handler);
	}

	/**
	 * This method register an event handler for a provided queue.
	 * 
	 * @param queueExpression The expression matching one or more queues. The
	 *        queue could not be yet created.
	 * @param qEventHandler The handler.
	 * @throws NameParseException In case the provided queue name is not a
	 *         valid.
	 */
	public void addQueueEventHandler(String queueExpression, QueueEventHandler qEventHandler) throws NameParseException
	{
		if (!Pattern.matches("((\\w+)\\.)*((\\w+)|\\*)", queueExpression))
			throw new NameParseException("Invalid queue name: " + queueExpression);

		if (!this.queueEventHandlers.containsKey(queueExpression))
			this.queueEventHandlers.put(queueExpression, new ArrayList<QueueEventHandler>());

		this.queueEventHandlers.get(queueExpression).add(qEventHandler);
	}

	@Override
	protected void finalize() throws Throwable
	{
		super.finalize();
		this.executor.shutdownNow();
	}
}
